/**@file
   Kernel stub device class
   It is only for windows OS.

   @par license 
   Copyright 2012 Lu, Ken. All rights reserved.

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
 
   @author: Lu, Ken (bluewish.ken.lu@gmail.com)
**/
#include <Windows.h>
#include <assert.h>

#include "userbase/logger.h"
#include "userbase/wdfhelper.h"

#include "kstub.h"
#include "kstub_if.h"

namespace ken {

#define DRIVER_NAME         L"kstub"
#define DEVICE_NAME         L"\\\\.\\kstub"
#define INF_NAME            L"kstub.inf"
#define WDF_SECTION_NAME    L"kstub.NT.wdf"

u_KStubDev* u_KStubDev::instance_   = 0;
wstring     u_KStubDev::driver_dir_ = L"";

u_KStubDev::u_KStubDev():device_(NULL)
{
    u_func_enter();

    if (u_KStubDev::driver_dir_ == L"") {
        wchar_t path[MAX_PATH] = {0};
        ::GetCurrentDirectoryW(MAX_PATH, path);
        u_KStubDev::driver_dir_ = path;

    }
    load();

    u_func_leave();
}

u_KStubDev::~u_KStubDev()
{
    u_func_enter();

    if (device_ != NULL) {
        ::CloseHandle(device_);
    }
    
    unload();

    u_func_leave();

}

u32_t u_KStubDev::ioread(u16_t port, u8_t width)
{
    u32_t value;
    bool  ret;
    uint32_t returnedbytes;

    u_func_enter();

    assert(device_ != NULL);
    assert(width <= 4);

    IO_PARAM param = {0};
    param.port = port;
    param.width = width;

    ret = ::DeviceIoControl(
        device_, 
        KS_DRIVER_IO_READ, 
        &param, 
        sizeof(IO_PARAM), 
        &value, 
        4, 
        (LPDWORD)&returnedbytes, 
        NULL
        ) == TRUE;
    if (!ret) {
        char message[1024] = {0};
        sprintf_s(message, 1024, "Fail to ioread:0x%x, width:0x%x,"
                 "error code=0x%x", port, width, ::GetLastError());
        throw runtime_error(message);
    }

    u_func_leave();

    return value;
}

void u_KStubDev::iowrite(u16_t port, u8_t width, u32_t in)
{
    bool ret;
    uint32_t returnedbytes;

    u_func_enter();

    assert(device_ != NULL);
    assert(width <= 4);

    IO_PARAM param = {0};
    param.port  = port;
    param.width = width;
    param.value = in;

    ret = ::DeviceIoControl(
        device_, 
        KS_DRIVER_IO_WRITE, 
        &param, 
        sizeof(IO_PARAM), 
        NULL, 
        0, 
        (LPDWORD)&returnedbytes, 
        NULL
        ) == TRUE;
    if (!ret) {
        char message[1024] = {0};
        sprintf_s(message, 1024, "Fail to write:0x%x, width:0x%x,"
                 "error code=0x%x", port, width, ::GetLastError());
        throw runtime_error(message);
    }

    u_func_leave();
}

void u_KStubDev::load()
{
    u_func_enter();

    if (device_ != NULL) {
        return;
    }

    device_ = ::CreateFileW(DEVICE_NAME, GENERIC_READ | GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
    if (device_ == INVALID_HANDLE_VALUE) {
        uint32_t err = ::GetLastError();
        if (err != ERROR_FILE_NOT_FOUND && err != ERROR_PATH_NOT_FOUND) {
            throw runtime_error("Fail to create device");
        }
        WdfDriverHelper driver_helper(DEVICE_NAME, DRIVER_NAME);
        driver_helper.Initialize();
        char path1[MAX_PATH] = {0};
        char path2[MAX_PATH] = {0};
        u_info(
            "Install new driver %s\\%s", 
            driver_helper.u2a((uint16_t*)driver_dir_.c_str(), (uint8_t*)&path1[0]),
            driver_helper.u2a((uint16_t*)INF_NAME, (uint8_t*)&path2[0])
            );
        try {
            driver_helper.Install(driver_dir_.c_str(), INF_NAME, WDF_SECTION_NAME);
            driver_helper.Start();
        } catch (runtime_error err) {
            u_error(err.what());
            throw runtime_error("Fail to install driver");
        }

        device_ = ::CreateFileW(DEVICE_NAME, GENERIC_READ | GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
    }

    u_func_leave();
}

void u_KStubDev::unload()
{
    WdfDriverHelper driver_helper(DEVICE_NAME, DRIVER_NAME);

    u_func_enter();

    driver_helper.Initialize();
    driver_helper.Stop();
    driver_helper.Remove(driver_dir_.c_str(), INF_NAME, WDF_SECTION_NAME);

    u_func_leave();
}

void u_KStubDev::mmioread(un_t base, u32_t &length, u8_t *out)
{
    bool        ret;
    uint32_t    readlen, current, returnedbytes, adjust;
    u64_t       temp;

    assert (device_ != NULL);
    assert (out != NULL);
    assert (base < 0x100000000ULL);

    temp = (u64_t)length + (u64_t)base;

    if (temp > 0x100000000ULL) {
        adjust = (u32_t)(0x100000000ULL - base);
        u_warn("The length 0x%x is shorten to 0x%x to avoid exceed 4G", length, adjust);
    } else {
        adjust = length;
    }

    current = base;
    do {
        readlen = base + adjust - current;
        readlen = readlen < 0x100000 ? readlen : 0x100000;
        MMIO_READ_PARAM param = {current, readlen};
        returnedbytes = readlen;
        ret = ::DeviceIoControl(
            device_, 
            KS_DRIVER_MMIO_READ, 
            &param, 
            sizeof(MMIO_READ_PARAM), 
            out + current - base, 
            adjust - (current - base), 
            (LPDWORD)&returnedbytes, 
            NULL
            ) == TRUE;
        if (!ret) {
            u_throw("Fail to get mapped memory base:0x%x, length:0x%x,"
                    "error code=0x%x", base, length, ::GetLastError());
        }
        current += returnedbytes;
    } while (current < base + adjust);

    length = adjust;
}

void  u_KStubDev::mmiowrite(un_t base, u32_t length, u8_t *in)
{
    throw runtime_error("Not implement");
}

bool  u_KStubDev::mmioscan(un_t base, u32_t length, u8_t *in, u32_t inlen, u32_t &first)
{
    bool ret;
    uint32_t returnedbytes;

    assert(device_ != NULL);

    MMIO_SCAN_PARAM param = {0};
    param.start = base;
    param.length = length;
    param.scan_buffer_len = inlen;
    memcpy(param.scan_buffer, in, inlen);
    
    ret = ::DeviceIoControl(
        device_, 
        KS_DRIVER_MMIO_SCAN, 
        &param, 
        sizeof(MMIO_SCAN_PARAM), 
        &first, 
        sizeof(uint32_t), 
        (LPDWORD)&returnedbytes, 
        NULL
        ) == TRUE;
    if (!ret) {
        if (::GetLastError() == ERROR_NOT_FOUND) {
            return false;
        } else {
            u_throw("Fail to ioctl for Mmio scan, code=0x%x", ::GetLastError());
        }
    }
    return true;        
}

} // namespace